import * as path from 'path';
import { TextEncoder } from 'util';
import * as vscode from 'vscode';
import { Disposable, disposeAll } from './dispose';
import * as fs from "fs";
import * as mime from "mime";

type GetFileData = () => Promise<string>;
type EditHistory = {
    content: string;
    context: string;
}
/**
 * Define the document (the data model) used for paw draw files.
 */
class SVGDocument extends Disposable implements vscode.CustomDocument {

    static async create(
        uri: vscode.Uri,
        backupId: string | undefined,
        getFileData: GetFileData,
    ): Promise<SVGDocument | PromiseLike<SVGDocument>> {
        // If we have a backup, read that. Otherwise read the resource from the workspace
        const dataFile = typeof backupId === 'string' ? vscode.Uri.parse(backupId) : uri;
        const fileData = (await SVGDocument.readFile(dataFile)).toString();
        return new SVGDocument(uri, fileData, getFileData);
    }

    private static async readFile(uri: vscode.Uri): Promise<string> {
        if (uri.scheme === 'untitled') {
            return '';
        }
        return (await vscode.workspace.fs.readFile(uri)).toString();
    }

    private readonly _uri: vscode.Uri;

    private _documentData: string;
    private readonly _getFileData: GetFileData;

    private constructor(
        uri: vscode.Uri,
        initialContent: string,
        getFileData: GetFileData
    ) {
        super();
        this._uri = uri;
        this._documentData = initialContent;
        this._getFileData = getFileData;
    }

    public get uri() { return this._uri; }

    public get documentData(): string { return this._documentData; }

    private readonly _onDidDispose = this._register(new vscode.EventEmitter<void>());
    /**
     * Fired when the document is disposed of.
     */
    public readonly onDidDispose = this._onDidDispose.event;

    private readonly _onDidChangeDocument = this._register(new vscode.EventEmitter<EditHistory>());
    /**
     * Fired to notify webviews that the document has changed.
     */
    public readonly onDidChangeContent = this._onDidChangeDocument.event;

    private readonly _onDidChange = this._register(new vscode.EventEmitter<{
        readonly label: string,
        undo(): void,
        redo(): void
    }>());
    /**
     * Fired to tell VS Code that an edit has occured in the document.
     * 
     * This updates the document's dirty indicator.
     */
    public readonly onDidChange = this._onDidChange.event;

    /**
     * Called by VS Code when there are no more references to the document.
     * 
     * This happens when all editors for it have been closed.
     */
    dispose(): void {
        this._onDidDispose.fire();
        super.dispose();
    }
    private _editors: EditHistory[] = [];
    /**
     * Called when the user edits the document in a webview.
     * 
     * This fires an event to notify VS Code that the document has been edited.
     */

    makeEdit(content: EditHistory) {
        this._editors.push(content);
        this._onDidChange.fire({
            label: 'SvgEdited',
            undo: async () => {
                let str = this._editors.pop();
                let len = this._editors.length
                if (len) {
                    str = this._editors[len - 1];
                } else {
                    str = { content: this._documentData, context: "" };
                }
                this._onDidChangeDocument.fire(str)
            },
            redo: async () => {
                this._editors.push(content);
                this._onDidChangeDocument.fire(content)
            }
        });
    }

    /**
     * Called by VS Code when the user saves the document.
     */
    async save(cancellation: vscode.CancellationToken): Promise<void> {
        await this.saveAs(this.uri, cancellation);
    }

    /**
     * Called by VS Code when the user saves the document to a new location.
     */
    async saveAs(targetResource: vscode.Uri, cancellation: vscode.CancellationToken): Promise<void> {
        let fileData = await this._getFileData();
        if (cancellation.isCancellationRequested) {
            return;
        }
        await vscode.workspace.fs.writeFile(targetResource, new TextEncoder().encode(fileData));
    }

    /**
     * Called by VS Code when the user calls `revert` on a document.
     */
    async revert(_cancellation: vscode.CancellationToken): Promise<void> {
        const diskContent = await SVGDocument.readFile(this.uri);
        this._documentData = diskContent;
        this._onDidChangeDocument.fire({
            content: diskContent,
            context: ""
        });
    }

    /**
     * Called by VS Code to backup the edited document.
     * 
     * These backups are used to implement hot exit.
     */
    async backup(destination: vscode.Uri, cancellation: vscode.CancellationToken): Promise<vscode.CustomDocumentBackup> {
        await this.saveAs(destination, cancellation);

        return {
            id: destination.toString(),
            delete: async () => {
                try {
                    await vscode.workspace.fs.delete(destination);
                } catch {
                    // noop
                }
            }
        };
    }
}

/**
 * Provider for SVG editors.
 * 
 * SVGSVG editors are used for `.svg` files, which are just `.png` files with a different file extension.
 * 
 * This provider demonstrates:
 * 
 * - How to implement a custom editor for binary files.
 * - Setting up the initial webview for a custom editor.
 * - Loading scripts and styles in a custom editor.
 * - Communication between VS Code and the custom editor.
 * - Using CustomDocuments to store information that is shared between multiple custom editors.
 * - Implementing save, undo, redo, and revert.
 * - Backing up a custom editor.
 */
export class SVGEditorProvider implements vscode.CustomEditorProvider<SVGDocument> {

    private static newPawDrawFileId = 1;

    public static register(context: vscode.ExtensionContext): vscode.Disposable {
        vscode.commands.registerCommand('webscada.svgeditor.new', () => {
            const workspaceFolders = vscode.workspace.workspaceFolders;
            if (!workspaceFolders) {
                vscode.window.showErrorMessage("Creating new Paw Draw files currently requires opening a workspace");
                return;
            }

            const uri = vscode.Uri.joinPath(workspaceFolders[0].uri, `new-${SVGEditorProvider.newPawDrawFileId++}.svg`)
                .with({ scheme: 'untitled' });

            vscode.commands.executeCommand('vscode.openWith', uri, SVGEditorProvider.viewType);
        });

        return vscode.window.registerCustomEditorProvider(
            SVGEditorProvider.viewType,
            new SVGEditorProvider(context),
            {
                // For this demo extension, we enable `retainContextWhenHidden` which keeps the 
                // webview alive even when it is not visible. You should avoid using this setting
                // unless is absolutely required as it does have memory overhead.
                webviewOptions: {
                    retainContextWhenHidden: true,
                },
                supportsMultipleEditorsPerDocument: false,
            });
    }

    private static readonly viewType = 'webscada.svgeditor';

    /**
     * Tracks all known webviews
     */
    private readonly webviews = new WebviewCollection();

    constructor(
        private readonly _context: vscode.ExtensionContext
    ) { }

    //#region CustomEditorProvider

    async openCustomDocument(
        uri: vscode.Uri,
        openContext: { backupId?: string },
        _token: vscode.CancellationToken
    ): Promise<SVGDocument> {
        const document: SVGDocument = await SVGDocument.create(uri, openContext.backupId,
            async () => {
                const webviewsForDocument = Array.from(this.webviews.get(document.uri));
                if (!webviewsForDocument.length) {
                    throw new Error('Could not find webview to save for');
                }
                const panel = webviewsForDocument[0];
                const response = await this.postMessageWithResponse<string>(panel, 'getFileData', {});
                return response;

            }
        );

        const listeners: vscode.Disposable[] = [];

        listeners.push(document.onDidChange(e => {
            // Tell VS Code that the document has been edited by the use.
            this._onDidChangeCustomDocument.fire({
                document,
                ...e
            });
        }));

        listeners.push(document.onDidChangeContent(e => {
            // Update all webviews when the document changes
            for (const webviewPanel of this.webviews.get(document.uri)) {
                this.postMessage(webviewPanel, 'update', e);
            }
        }));

        document.onDidDispose(() => disposeAll(listeners));

        return document;
    }

    async resolveCustomEditor(
        document: SVGDocument,
        webviewPanel: vscode.WebviewPanel,
        _token: vscode.CancellationToken
    ): Promise<void> {
        // Add the webview to our internal set of active webviews
        this.webviews.add(document.uri, webviewPanel);

        // Setup initial content for the webview
        webviewPanel.webview.options = {
            enableScripts: true,
        };
        webviewPanel.webview.html = await this.getHtmlForWebview(webviewPanel.webview);

        webviewPanel.webview.onDidReceiveMessage(e => this.onMessage(document, e));

        // Wait for the webview to be properly ready before we init
        webviewPanel.webview.onDidReceiveMessage(e => {
            if (e.type === 'ready') {
                if (document.uri.scheme !== 'untitled' && !!document.documentData) {
                    this.postMessage(webviewPanel, 'update', { content: document.documentData });
                }
                if (vscode.workspace.workspaceFolders) {
                    let fd = vscode.workspace.workspaceFolders[0];
                    let fn = path.join(fd.uri.fsPath, "src/TagsDefine/tags.d.ts");
                    if (fs.existsSync(fn)) {
                        fs.readFile(fn, { encoding: 'utf-8' }, (_, data) => {
                            data = data.replace(/[\t ]+/g, "");
                            let mach = /app:\{\n([\s\S]*?)\}/.exec(data);
                            if (mach) {
                                let fields = mach[1].split(/:.+\n/);
                                let plcTags = fields.filter(value => value && !value.includes('_Quality') && !value.includes('_TimeStamp'));
                                webviewPanel.webview.postMessage({
                                    type: 'plcTags',
                                    plcTags
                                })
                            }
                        })
                    }
                }
            }
        });
    }

    private readonly _onDidChangeCustomDocument = new vscode.EventEmitter<vscode.CustomDocumentEditEvent<SVGDocument>>();
    public readonly onDidChangeCustomDocument = this._onDidChangeCustomDocument.event;

    public saveCustomDocument(document: SVGDocument, cancellation: vscode.CancellationToken): Thenable<void> {
        return document.save(cancellation);
    }

    public saveCustomDocumentAs(document: SVGDocument, destination: vscode.Uri, cancellation: vscode.CancellationToken): Thenable<void> {
        return document.saveAs(destination, cancellation);
    }

    public revertCustomDocument(document: SVGDocument, cancellation: vscode.CancellationToken): Thenable<void> {
        return document.revert(cancellation);
    }

    public backupCustomDocument(document: SVGDocument, context: vscode.CustomDocumentBackupContext, cancellation: vscode.CancellationToken): Thenable<vscode.CustomDocumentBackup> {
        return document.backup(context.destination, cancellation);
    }

    //#endregion

    /**
     * Get the static HTML used for in our editor's webviews.
     */
    private async preProcess(baseDir: string, fn: string, webview: vscode.Webview): Promise<string> {
        let str = fs.readFileSync(fn).toString();
        str = str.replace(/((?:url\(vscode-webview-resource:)|(?:url\())([^:]*?)\)/g, (_, $1, $2) => {
            if ($1.includes("vscode-webview-resource:")) {
                $2 = $2.slice($2.indexOf("media/") + 6);
            }
            return "url(" + webview.asWebviewUri(vscode.Uri.file(path.join(baseDir, $2))) + ")";
        })
        fs.writeFileSync(fn, str);
        return ('ok');
    }
    private async getHtmlForWebview(webview: vscode.Webview): Promise<string> {
        const resourcePath = path.join(this._context.extensionPath, 'media/index.html');
        const dirPath = path.dirname(resourcePath);
        let html = fs.readFileSync(resourcePath, 'utf-8');
        let promises: Promise<string>[] = [];
        html = html.replace(/(<link.+?href="|<script.+?src="|<img.+?src=")(.+?)(".*)/g, (_, $1, $2, $3) => {
            let fn = $2.slice(0, $2.indexOf("?"));
            if (path.extname(fn) === ".css") {
                let promise = this.preProcess(dirPath, path.join(dirPath, fn), webview);
                promises.push(promise);
            }
            let s = webview.asWebviewUri(vscode.Uri.file(path.join(dirPath, $2)));
            return $1 + s.scheme + "://" + s.authority + s.path + $3;
        });
        await Promise.all(promises);
        return html;
    }

    private _requestId = 1;
    private readonly _callbacks = new Map<number, (response: any) => void>();

    private postMessageWithResponse<R = unknown>(panel: vscode.WebviewPanel, type: string, body: any): Promise<R> {
        const requestId = this._requestId++;
        const p = new Promise<R>(resolve => this._callbacks.set(requestId, resolve));
        panel.webview.postMessage({ type, requestId, body });
        return p;
    }

    private postMessage(panel: vscode.WebviewPanel, type: string, body: any): void {
        panel.webview.postMessage({ type, body });
    }

    private onMessage(document: SVGDocument, message: any) {
        switch (message.type) {
            case 'changed':
                document.makeEdit({ content: message.content, context: message.currentContext });
                return;
            case 'response':
                {
                    const callback = this._callbacks.get(message.requestId);
                    this._callbacks.delete(message.requestId);
                    callback?.(message.body);
                    return;
                }
            case 'importImage':
                {
                    let panels = this.webviews.get(document.uri);
                    vscode.window.showOpenDialog({ filters: { Images: ['svg', 'jpg', 'png'] } })
                        .then(url => {
                            if (url) {
                                let fn = url[0].fsPath
                                let type = mime.getType(fn);
                                if (!type!.includes("image")) return;
                                let buf = fs.readFileSync(fn);
                                if (type!.includes("svg")) {
                                    for (const panel of panels) {
                                        this.postMessage(panel, 'importImage', { filetype: type, content: buf.toString('utf-8') })
                                    }
                                } else {

                                    let content = `data:${type};base64,` + buf.toString("base64");
                                    for (const panel of panels) {
                                        this.postMessage(panel, 'importImage', { filetype: type, content });
                                    }
                                }
                            }
                        })
                    return;
                }
            case 'message':
                vscode.window.showErrorMessage(message.msg);
                return;
        }
    }
}

/**
 * Tracks all webviews.
 */
class WebviewCollection {

    private readonly _webviews = new Set<{
        readonly resource: string;
        readonly webviewPanel: vscode.WebviewPanel;
    }>();

    /**
     * Get all known webviews for a given uri.
     */
    public *get(uri: vscode.Uri): Iterable<vscode.WebviewPanel> {
        const key = uri.toString();
        for (const entry of this._webviews) {
            if (entry.resource === key) {
                yield entry.webviewPanel;
            }
        }
    }

    /**
     * Add a new webview to the collection.
     */
    public add(uri: vscode.Uri, webviewPanel: vscode.WebviewPanel) {
        const entry = { resource: uri.toString(), webviewPanel };
        this._webviews.add(entry);

        webviewPanel.onDidDispose(() => {
            this._webviews.delete(entry);
        });
    }
}